ViewBinding--压死 findViewById 的最后一根稻草

Jetpack 是谷歌推出一套库、工具和指南,可以帮助开发者更轻松地编写优质应用,摆脱编写样板代码的工作并简化复杂任务,将精力集中放在所需的代码上

简而言之,可以理解为是官方提供的一套 Android 开发脚手架,在这个新技术层出不群,质量也无法保证的时代,跟着官方的建议走,也未尝不是一件坏事。视图绑定属于 Jetpack 中的架构组件,主要就是用来快速方便的引用视图,精准定位,彻底帮你省去 findViewById

使用

ViewBinding 是在 Android Gradle 插件 3.6.0 版本中引入,因此要确保 Android Studio 和 Gradle 插件版本号大于等于 3.6.0。

接着,需要在模块的 build.gradle 文件下启用:

1
2
3
4
5
6
android {
...
viewBinding {
enabled = true
}
}

Android Gradle 插件 4.0.0-alpha05 引入了buildFeatures 块来控制构建功能,如视图绑定、数据绑定和 Jetpack Compose,需要如下这样配置:

1
2
3
4
5
6
7
8
9
android {
// The default value for each feature is shown below. You can change the value to
// override the default behavior.
buildFeatures {
// Determines whether to support View Binding.
// Note that the viewBinding.enabled property is now deprecated.
viewBinding = false
}
}

此时,当创建一个 XML 布局文件后,就会以 XML 文件名称按照驼峰大小形式来自动生成一个绑定类,例如:activity_main.xml对应的是 ActivityMainBinding.javaactivityuser.xml 对应 Activityuser.java

通过调用对应 Binding 类的 inflate() 方法,并传入参数 LayoutInflater 来获取一个实例。

1
2
3
4
5
6
7
private lateinit var viewBinding : ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
}

可以通过 getRoot() 来获取布局的根View,这里 ViewBinding 还提供了接受三个参数的 inflate() 方法,与 LayoutInflater 的接口一致。在获取到实例后,就可以通过 binding.id 来获取到对应 ID 的 View 实例了。

1
viewBinding.my_text.setText("hello, world.")

默认情况下,会为每个布局 XML 文件都生成绑定类,可以通过将 tools:viewBindingIgnore="true" 属性添加到布局文件的根视图来忽略生成。

分析

既然 ViewBinding 的作用是用来减少写 findViewById 样板代码,那它就免不了 ButterKnife 做一下对比了,官方给出的优点是:

  • Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。
  • 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。

如下面的代码所示,在声明成员变量时,会根据 XML 文件中的 View 类型来创建,因此不会存在类型转换的问题,并且会添加注解 @NonNull

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@NonNull
private final ConstraintLayout rootView;

@NonNull
public static ActivityTestBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_test, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}

@NonNull
public static ActivityTestBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
String missingId;
missingId: {
ImageView activityTestImg = rootView.findViewById(R.id.activity_test_img);
if (activityTestImg == null) {
missingId = "activityTestImg";
break missingId;
}
return new ActivityTestBinding((ConstraintLayout) rootView, activityTestImg,
activityTestInclude);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}

在 ViewBinding 的 inflate() 中,会去调用另一个静态方法 bind(),在这里,会通过 findViewById 来获取 View,如果为空,则会跑出异常,保证了在编译期间报错,而不是在运行期间。

更为关键的是,不用依赖任何库,这就大大减少了应用引入的成本。目前 ButterKnife 的 Github 主页上已经有了备注,建议考虑下是否替换为 ViewBinding。

Attention: Development on this tool is winding down. Please consider switching to view binding in the coming months.

使用时可以发现,每次编辑完 XML 文件,例如添加一个带 ID 的 View 后,在 Java 文件中就可以通过对应的绑定类调用到,这是由于 ViewBinding 的代码生成插件会优先在内存中创建一个该 XML 的 Binding 对象,并对其进行更新,而并非是在 Build 之后才去创建。

与 DataBinding 的区别

使用 DataBinding 和使用 ViewBinding 所生成的绑定类名称是一样的,通过绑定类中的注释来看,并非是使用同一编译器生成。

1
2
// Generated by data binding compiler. Do not edit!
// Generated by view binder compiler. Do not edit!

ViewBinding 的使用场景很单一,就是用来简化样板代码,其绑定类是实现了androidx.viewbinding.ViewBinding 接口

1
2
3
4
5
6
7
8
9
/** A type which binds the views in a layout XML to fields. */
public interface ViewBinding {
/**
* Returns the outermost {@link View} in the associated layout file. If this binding is for a
* {@code <merge>} layout, this will return the first view inside of the merge tag.
*/
@NonNull
View getRoot();
}

而 DataBinding 的绑定类则是继承自 androidx.databinding.ViewDataBinding,并且 ViewDataBinding 是实现了 ViewBinding 接口,也就是说通过 DataBinding 也可以实现绑定视图,但需要注意的是,DataBinding 仅处理使用 layout 代码创建的数据绑定布局,即如果在布局中有 include 标签,并设置了 ID,那么如果包含的布局是没有在 layout 中,那么是调用不到 include 布局中的 View的。举个例子:

如果只打开了 ViewBinding,此时有两个布局,test_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
<!--test_main.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<include
android:id="@+id/test_main_include"
layout="@layout/test_include" />

</androidx.constraintlayout.widget.ConstraintLayout>

test_include.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/test_include_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

</androidx.constraintlayout.widget.ConstraintLayout>

那么此时是可以通过 binding.test_main_include.test_include_image 来获取到 test_main.xml 布局所包含的布局中的元素的,因为通过 binding.test_main_include 获取到的是 TestIncludeBinding,但如果此时只是使用 DataBinding,并且只有 test_main.xml 布局是在 layout 中,则此时调用 binding.test_main_include 获取到的只是 test_include.xml 布局的根 View ConstraintLayout 的实例,而无法获取到它的子 View,此时必须将 test_include.xml 也放在 layout 中,才可以正常获取到 test_include.xml 中的其他 View,或者也可以同时打开 ViewBinding 和 DataBinding 来实现。

参考

Android 开发者文档指南:视图绑定

Use view binding to replace findViewById

Android 开发者文档指南:数据绑定